Skip to content

fix(noema): fail closed at the credential egress boundary - #1279

Open
seonghobae wants to merge 5 commits into
mainfrom
codex/pr930-current-main-replacement-20260824
Open

fix(noema): fail closed at the credential egress boundary#1279
seonghobae wants to merge 5 commits into
mainfrom
codex/pr930-current-main-replacement-20260824

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Outcome

Replaces the conflicted and scope-polluted #930 with a clean current-main change.

  • requires HTTPS for every non-loopback model endpoint
  • resolves the effective host and port before constructing the credentialed request
  • rejects resolver failures, empty or malformed answers, and every non-global or multicast address
  • requires exact pre/post-request DNS-set equality
  • bounds provider responses to 1 MiB before JSON decoding
  • keeps redirect refusal and the existing Wire Noema to a same-job contextual-orchestrator sidecar #1120 same-job sidecar seam, restricted to literal 127.0.0.1 and ::1

This PR does not import contextual-orchestrator, alter provider routing, change model selection, or modify model parameters.

Evidence

  • 1,419 passed, 1 skipped, 16 subtests passed
  • 8,378 statements and 3,228 branches at 100%
  • scripts/ci docstrings: 100%
  • compileall and git diff --check: pass
  • APA 7th doctoring records OWASP SSRF guidance, RFC 6890, CWE-400, and CWE-918, including the residual DNS/socket TOCTOU boundary

Supersedes #930. Preserves the trusted loopback consumer contract from #1120.


Open in Devin Review

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 43 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d9854b2-3175-4989-a7b7-d5f6e0b1eb6d

📥 Commits

Reviewing files that changed from the base of the PR and between 1d8e872 and 3feb583.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/doctoring/noema-credential-egress-boundary.md
  • scripts/ci/noema_review_gate.py
  • tests/test_noema_endpoint_boundary.py
  • tests/test_noema_review_gate.py
  • tests/test_noema_review_orchestrator_ssrf.py
  • tests/test_repository_branch_coverage_review_schedulers.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.

Findings

1. HIGH Current-head GitHub Checks - Fix failed required checks before approval

  • Problem: Failed same-head checks remain for b19c5b452cf53a5b5a85d9805efaa1899cf0a04b.
  • Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.
  • Fix: Read and fix the failed check logs below, then rerun the current-head checks.
  • Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.

Failed checks:

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: CHANGELOG.md"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: CHANGELOG.md"]
  R1 --> V1["required checks"]
  Evidence --> S2["Docs: noema-credential-egress-boundary.md"]
  S2 --> I2["operator or user guidance"]
  I2 --> R2["Review risk: Docs: noema-credential-egress-boundary.md"]
  R2 --> V2["docs review"]
  Evidence --> S3["CI script: noema_review_gate.py"]
  S3 --> I3["review and security gate shell path"]
  I3 --> R3["Review risk: CI script: noema_review_gate.py"]
  R3 --> V3["bash -n plus Strix self-test"]
  Evidence --> S4["Test (3 files)"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test (3 files)"]
  R4 --> V4["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 721a36f24616343029a291f02db32610f470a884
  • Workflow run: 32746125713
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.

Findings

1. HIGH Current-head GitHub Checks - Fix failed required checks before approval

  • Problem: Failed same-head checks remain for 721a36f24616343029a291f02db32610f470a884.
  • Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.
  • Fix: Read and fix the failed check logs below, then rerun the current-head checks.
  • Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.

Failed checks:

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: CHANGELOG.md"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: CHANGELOG.md"]
  R1 --> V1["required checks"]
  Evidence --> S2["Docs: noema-credential-egress-boundary.md"]
  S2 --> I2["operator or user guidance"]
  I2 --> R2["Review risk: Docs: noema-credential-egress-boundary.md"]
  R2 --> V2["docs review"]
  Evidence --> S3["CI script: noema_review_gate.py"]
  S3 --> I3["review and security gate shell path"]
  I3 --> R3["Review risk: CI script: noema_review_gate.py"]
  R3 --> V3["bash -n plus Strix self-test"]
  Evidence --> S4["Test (3 files)"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test (3 files)"]
  R4 --> V4["targeted test run"]
Loading

@seonghobae
seonghobae enabled auto-merge (squash) August 24, 2026 02:50
@opencode-agent
opencode-agent Bot disabled auto-merge August 24, 2026 03:14
devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae
seonghobae enabled auto-merge (squash) August 24, 2026 03:34
devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head formal review request for 92c8b95. Inspect the complete current diff, especially Noema credential egress boundary, fail-closed behavior, and secret handling. Publish a substantive Reviews API verdict tied only to this exact SHA, with changed-file walkthrough, security/control-plane impact, test gaps, and residual uncertainty. Do not reuse predecessor evidence. @opencode-agent

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head current identity is 92c8b95e5778dda51a60f483162be3ec40c2aa23. The prior Strix failure run 32692202410 is provider infrastructure only: repeated NVIDIA NIM 429 rate limits followed by direct fallback 404, with fail-closed no-report behavior. I reran the failed job normally. The pinned transport/SSRF change has no unresolved current thread; please publish a substantive exact-head review after the rerun. No bypass or merge until all required Checks and approval are current.

@seonghobae
seonghobae force-pushed the codex/pr930-current-main-replacement-20260824 branch from 92c8b95 to 2adc8c4 Compare August 24, 2026 11:24
devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head validation after fixing the Devin IPv6 finding:

  • Exact head: 721a36f24616343029a291f02db32610f470a884
  • Exact base: 613a33e0cb1c6db9790fae99f6253445712ac37a
  • Root cause fixed: _socket_target now passes validated IPv4 and IPv6 literals to socket.create_connection as (host, port). The prior IPv6 four-field tuple raises ValueError before connection and bypasses the OSError retry path.
  • Regression coverage: test_pinned_connection_supports_ipv6_destination_shape now drives PinnedHTTPConnection.connect() and asserts the exact IPv6 target.
  • Focused validation: 57 passed in tests/test_noema_endpoint_boundary.py, tests/test_noema_review_gate.py, and tests/test_repository_branch_coverage_review_schedulers.py.
  • compileall, git diff --check, and clean-worktree checks passed.

The remaining TCP_NODELAY note is informational and does not affect credential routing or correctness; it is not being expanded into this bounded fix. Hosted checks and a fresh independent review are still pending, so this PR is not being merged or force-merged.

@opencode-agent
opencode-agent Bot disabled auto-merge August 24, 2026 13:18

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.

Findings

1. HIGH Current-head GitHub Checks - Fix failed required checks before approval

  • Problem: Failed same-head checks remain for 721a36f24616343029a291f02db32610f470a884.
  • Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.
  • Fix: Read and fix the failed check logs below, then rerun the current-head checks.
  • Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.

Failed checks:

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: CHANGELOG.md"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: CHANGELOG.md"]
  R1 --> V1["required checks"]
  Evidence --> S2["Docs: noema-credential-egress-boundary.md"]
  S2 --> I2["operator or user guidance"]
  I2 --> R2["Review risk: Docs: noema-credential-egress-boundary.md"]
  R2 --> V2["docs review"]
  Evidence --> S3["CI script: noema_review_gate.py"]
  S3 --> I3["review and security gate shell path"]
  I3 --> R3["Review risk: CI script: noema_review_gate.py"]
  R3 --> V3["bash -n plus Strix self-test"]
  Evidence --> S4["Test (3 files)"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test (3 files)"]
  R4 --> V4["targeted test run"]
Loading

…ain-replacement-20260824

# Conflicts:
#	CHANGELOG.md
#	scripts/ci/noema_review_gate.py

Copy link
Copy Markdown
Contributor Author

Resolved the stale merge conflict against main

Merged origin/main (1d8e8724) into this PR's head via a merge commit (3feb583e), pushed to the existing branch. Two files conflicted textually: CHANGELOG.md (additive, kept both entries) and scripts/ci/noema_review_gate.py (one real design conflict, resolved below).

What main already had vs. what this PR still uniquely contributes

Since this PR branched, main independently landed a narrower, exact-origin sidecar allowlist for the loopback/HTTP exception: is_allowed_orchestrator_sidecar_url() now requires a 127.0.0.1/::1 literal to match the exact configured CONTEXTUAL_ORCHESTRATOR_BASE_URL origin (scheme+host+port), not merely be a loopback literal on any port — see reject_private_llm_url() and _http_origin() in scripts/ci/noema_review_gate.py on main. That is a real security improvement over this PR's original TRUSTED_LOOPBACK_ADDRESSES check, which accepted any 127.0.0.1/::1 literal regardless of port.

main still had none of this PR's core contribution: no DNS-address pinning against rebinding (resolve_endpoint_addresses, PinnedHTTPConnection/PinnedHTTPSConnection, PinnedHTTPHandler/PinnedHTTPSHandler), no mandatory-HTTPS-for-non-loopback enforcement, no pre/post-request DNS-set equality check, and no response-size bound before JSON decoding.

Resolution: kept this PR's full pinned-connection/DNS-rebinding-protection machinery, but rewrote validate_endpoint() to decide the loopback exception via is_allowed_orchestrator_sidecar_url(api_url) (main's exact-origin check) instead of the broader "any loopback literal" check, so the merge doesn't regress the security improvement main shipped in the meantime. reject_private_llm_url() / is_allowed_orchestrator_sidecar_url() / _http_origin() / _is_loopback_literal_host() / _truthy_env() are kept intact as main's original standalone helpers (still directly unit-tested); call_llm() now calls only validate_endpoint().

Follow-on test updates needed for the merge to stay green (not textual conflicts, but consequences of the above):

  • tests/test_noema_review_orchestrator_ssrf.py: updated FakeResponse.read() to accept a bounded size arg (matches the new response.read(MAX_LLM_RESPONSE_BYTES + 1) call), updated two call_llm error-message assertions to the new fail-closed messages, and added two direct unit tests for reject_private_llm_url() branches that lost their transitive coverage now that call_llm no longer routes through it (the defensive non-http parsed-scheme guard, and the public/global-DNS non-raising path).
  • tests/test_noema_endpoint_boundary.py: the three sidecar-related tests now set CONTEXTUAL_ORCHESTRATOR_BASE_URL to the exact matching origin so the (now-stricter) sidecar exception still applies where intended, plus one new test asserting a loopback literal on a non-matching port is correctly treated as non-sidecar and rejected for using plaintext HTTP.

Test evidence (Python 3.12, this repo's actual target — see note below)

  • PYTHONPATH=. python -m pytest tests -q1931 passed, 1 skipped, 21 subtests passed
  • coverage run -m pytest tests -q && coverage report100% on scripts/ci/noema_review_gate.py (471 stmts / 184 branches) and 100% total across scripts/ci/** (10,057 stmts / 3,962 branches)
  • interrogate -v scripts/ci/noema_review_gate.py100% (55/55)
  • interrogate -c pyproject.toml .PASSED (100.0%)
  • python -m compileall and git diff --check → clean

Note: this sandbox's default python3 is 3.11, under which 2 of this PR's own PinnedHTTPSConnection tests fail with AttributeError: 'Context' object has no attribute 'verify_mode' — confirmed pre-existing on this PR's unmerged head too (not a merge regression), and it's an http.client.HTTPSConnection.__init__ stdlib difference between 3.11 and 3.12+. All the above evidence was gathered on a Python 3.12 venv, matching this repo's actual CI target for this tooling; both tests pass cleanly there.


Generated by Claude Code

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Devin Review

Comment on lines +492 to +526
class PinnedHTTPHandler(urllib.request.HTTPHandler):
"""urllib handler that uses validated numeric destinations for HTTP requests."""

def __init__(self, addresses: frozenset[IpAddress]) -> None:
"""Bind this handler to one prevalidated DNS result set."""
super().__init__()
self._addresses = addresses

def http_open(self, req: urllib.request.Request) -> Any:
"""Open an HTTP request without resolving its hostname again."""
return self.do_open(
lambda host, **kwargs: PinnedHTTPConnection(
host, validated_addresses=self._addresses, **kwargs
),
req,
)


class PinnedHTTPSHandler(urllib.request.HTTPSHandler):
"""urllib handler that pins TCP while preserving HTTPS hostname verification."""

def __init__(self, addresses: frozenset[IpAddress]) -> None:
"""Bind this handler to one prevalidated DNS result set."""
super().__init__()
self._addresses = addresses

def https_open(self, req: urllib.request.Request) -> Any:
"""Open HTTPS using the validated address set and original URL hostname."""
return self.do_open(
lambda host, **kwargs: PinnedHTTPSConnection(
host, validated_addresses=self._addresses, **kwargs
),
req,
context=self._context,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Validated transport remains internally consistent

Custom handlers replace urllib defaults and pin sockets to validated addresses. TLS still verifies the original hostname, while the exact loopback sidecar remains reachable.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +432 to +434
def _socket_target(address: IpAddress, port: int) -> tuple[str, int]:
"""Return a socket destination that contains only a validated IP literal."""
return (str(address), port)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: IPv6 destination shape is valid

socket.create_connection accepts a two-element host-and-port pair for IPv6 literals. The four-element form applies to lower-level socket addresses, not this API.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +768 to +769
if resolve_endpoint_addresses(hostname, port) != addresses:
raise ValueError("Noema endpoint DNS addresses changed during the request")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: DNS recheck cannot redirect the request

The post-response lookup only validates identity stability. The completed request already used the prevalidated numeric set, so later DNS changes cannot redirect it.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants